﻿using System;

using OpenTK;
using OpenTK.Graphics.ES20;
using GL1 = OpenTK.Graphics.ES11.GL;
using All1 = OpenTK.Graphics.ES11.All;
using OpenTK.Platform.iPhoneOS;

using Foundation;
using CoreAnimation;
using ObjCRuntime;
using OpenGLES;
using UIKit;

namespace MyOpenGLApp
{
	[Register ("EAGLView")]
	public class EAGLView : iPhoneOSGameView
	{
		[Export ("initWithCoder:")]
		public EAGLView (NSCoder coder) : base (coder)
		{
			LayerRetainsBacking = true;
			LayerColorFormat = EAGLColorFormat.RGBA8;
		}

		[Export ("layerClass")]
		public static new Class GetLayerClass ()
		{
			return iPhoneOSGameView.GetLayerClass ();
		}

		protected override void ConfigureLayer (CAEAGLLayer eaglLayer)
		{
			eaglLayer.Opaque = true;
		}

		protected override void CreateFrameBuffer ()
		{
			try {
				ContextRenderingApi = EAGLRenderingAPI.OpenGLES2;
				base.CreateFrameBuffer ();
			} catch (Exception) {
				ContextRenderingApi = EAGLRenderingAPI.OpenGLES1;
				base.CreateFrameBuffer ();
			}
			
			if (ContextRenderingApi == EAGLRenderingAPI.OpenGLES2)
				LoadShaders ();
		}

		protected override void DestroyFrameBuffer ()
		{
			base.DestroyFrameBuffer ();
			DestroyShaders ();
		}

		#region DisplayLink support

		int frameInterval;
		CADisplayLink displayLink;

		public bool IsAnimating { get; private set; }
		
		// How many display frames must pass between each time the display link fires.
		public int FrameInterval {
			get {
				return frameInterval;
			}
			set {
				if (value <= 0)
					throw new ArgumentException ();
				frameInterval = value;
				if (IsAnimating) {
					StopAnimating ();
					StartAnimating ();
				}
			}
		}

		public void StartAnimating ()
		{
			if (IsAnimating)
				return;
			
			CreateFrameBuffer ();
			displayLink = UIScreen.MainScreen.CreateDisplayLink (this, new Selector ("drawFrame"));
			displayLink.FrameInterval = frameInterval;
			displayLink.AddToRunLoop (NSRunLoop.Current, NSRunLoop.NSDefaultRunLoopMode);
			
			IsAnimating = true;
		}

		public void StopAnimating ()
		{
			if (!IsAnimating)
				return;

			displayLink.Invalidate ();
			displayLink = null;
			DestroyFrameBuffer ();
			IsAnimating = false;
		}

		[Export ("drawFrame")]
		void DrawFrame ()
		{
			OnRenderFrame (new FrameEventArgs ());
		}

		#endregion

		static readonly float[] squareVertices = {
			-0.5f, -0.33f,
			0.5f, -0.33f,
			-0.5f,  0.33f,
			0.5f,  0.33f,
		};
    
		static readonly byte[] squareColors = {
			255, 255,   0, 255,
			0,   255, 255, 255,
			0,     0,   0,   0,
			255,   0, 255, 255,
		};
		
		static float transY = 0.0f;
		
		const int UNIFORM_TRANSLATE = 0;
		const int UNIFORM_COUNT = 1;
		
		readonly int[] uniforms = new int [UNIFORM_COUNT];
		
		const int ATTRIB_VERTEX = 0;
		const int ATTRIB_COLOR = 1;
		const int ATTRIB_COUNT = 2;
		
		int program;

		protected override void OnRenderFrame (FrameEventArgs e)
		{
			base.OnRenderFrame (e);
			
			MakeCurrent ();
			
			// Replace the implementation of this method to do your own custom drawing.
			GL.ClearColor (0.5f, 0.5f, 0.5f, 1.0f);
			GL.Clear (ClearBufferMask.ColorBufferBit);
			
			if (ContextRenderingApi == EAGLRenderingAPI.OpenGLES2) {
				// Use shader program.
				GL.UseProgram (program);
				
				// Update uniform value.
				GL.Uniform1 (uniforms [UNIFORM_TRANSLATE], transY);
				transY += 0.075f;
				
				// Update attribute values.
				GL.VertexAttribPointer (ATTRIB_VERTEX, 2, VertexAttribPointerType.Float, false, 0, squareVertices);
				GL.EnableVertexAttribArray (ATTRIB_VERTEX);
				GL.VertexAttribPointer (ATTRIB_COLOR, 4, VertexAttribPointerType.UnsignedByte, true, 0, squareColors);
				GL.EnableVertexAttribArray (ATTRIB_COLOR);
				
				// Validate program before drawing. This is a good check, but only really necessary in a debug build.
#if DEBUG
				if (!ValidateProgram (program)) {
					Console.WriteLine ("Failed to validate program {0:x}", program);
					return;
				}
#endif
			} else {
				GL1.MatrixMode (All1.Projection);
				GL1.LoadIdentity ();
				GL1.MatrixMode (All1.Modelview);
				GL1.LoadIdentity ();
				GL1.Translate (0.0f, (float)Math.Sin (transY) / 2.0f, 0.0f);
				transY += 0.075f;
				
				GL1.VertexPointer (2, All1.Float, 0, squareVertices);
				GL1.EnableClientState (All1.VertexArray);
				GL1.ColorPointer (4, All1.UnsignedByte, 0, squareColors);
				GL1.EnableClientState (All1.ColorArray);
			}

			GL.DrawArrays (BeginMode.TriangleStrip, 0, 4);
			
			SwapBuffers ();
		}

		bool LoadShaders ()
		{
			int vertShader, fragShader;
			
			// Create shader program.
			program = GL.CreateProgram ();
			
			// Create and compile vertex shader.
			var vertShaderPathname = NSBundle.MainBundle.PathForResource ("Shader", "vsh");
			if (!CompileShader (ShaderType.VertexShader, vertShaderPathname, out vertShader)) {
				Console.WriteLine ("Failed to compile vertex shader");
				return false;
			}
			
			// Create and compile fragment shader.
			var fragShaderPathname = NSBundle.MainBundle.PathForResource ("Shader", "fsh");
			if (!CompileShader (ShaderType.FragmentShader, fragShaderPathname, out fragShader)) {
				Console.WriteLine ("Failed to compile fragment shader");
				return false;
			}

			// Attach vertex shader to program.
			GL.AttachShader (program, vertShader);
			
			// Attach fragment shader to program.
			GL.AttachShader (program, fragShader);
			
			// Bind attribute locations.
			// This needs to be done prior to linking.
			GL.BindAttribLocation (program, ATTRIB_VERTEX, "position");
			GL.BindAttribLocation (program, ATTRIB_COLOR, "color");
			
			// Link program.
			if (!LinkProgram (program)) {
				Console.WriteLine ("Failed to link program: {0:x}", program);
				
				if (vertShader != 0)
					GL.DeleteShader (vertShader);
				
				if (fragShader != 0)
					GL.DeleteShader (fragShader);
				
				if (program != 0) {
					GL.DeleteProgram (program);
					program = 0;
				}

				return false;
			}
			
			// Get uniform locations.
			uniforms [UNIFORM_TRANSLATE] = GL.GetUniformLocation (program, "translate");
			
			// Release vertex and fragment shaders.
			if (vertShader != 0) {
				GL.DetachShader (program, vertShader);
				GL.DeleteShader (vertShader);
			}
				
			if (fragShader != 0) {
				GL.DetachShader (program, fragShader);
				GL.DeleteShader (fragShader);
			}
			
			return true;
		}

		void DestroyShaders ()
		{
			if (program != 0) {
				GL.DeleteProgram (program);
				program = 0;
			}
		}

		#region Shader utilities

		static bool CompileShader (ShaderType type, string file, out int shader)
		{
			string src = System.IO.File.ReadAllText (file);

			shader = GL.CreateShader (type);
			GL.ShaderSource (shader, 1, new string[] { src }, (int[])null);
			GL.CompileShader (shader);
			
#if DEBUG
			int logLength;
			GL.GetShader (shader, ShaderParameter.InfoLogLength, out logLength);
			if (logLength > 0) {
				var infoLog = new System.Text.StringBuilder ();
				GL.GetShaderInfoLog (shader, logLength, out logLength, infoLog);
				Console.WriteLine ("Shader compile log:\n{0}", infoLog);
			}
#endif
			int status;
			GL.GetShader (shader, ShaderParameter.CompileStatus, out status);
			if (status == 0) {
				GL.DeleteShader (shader);
				return false;
			}
			
			return true;
		}

		static bool LinkProgram (int program)
		{
			GL.LinkProgram (program);
			
#if DEBUG
			int logLength;
			GL.GetProgram (program, ProgramParameter.InfoLogLength, out logLength);
			if (logLength > 0) {
				var infoLog = new System.Text.StringBuilder ();
				GL.GetProgramInfoLog (program, logLength, out logLength, infoLog);
				Console.WriteLine ("Program link log:\n{0}", infoLog);
			}
#endif
			int status;
			GL.GetProgram (program, ProgramParameter.LinkStatus, out status);
			if (status == 0)
				return false;
			
			return true;
		}

		static bool ValidateProgram (int program)
		{
			GL.ValidateProgram (program);
			
			int logLength;
			GL.GetProgram (program, ProgramParameter.InfoLogLength, out logLength);
			if (logLength > 0) {
				var infoLog = new System.Text.StringBuilder ();
				GL.GetProgramInfoLog (program, logLength, out logLength, infoLog);
				Console.WriteLine ("Program validate log:\n{0}", infoLog);
			}
			
			int status;
			GL.GetProgram (program, ProgramParameter.LinkStatus, out status);
			if (status == 0)
				return false;
			
			return true;
		}

		#endregion
	}
}
